/*
 *
 *  * Copyright (c) iwindplus Technologies Co., Ltd.2024-2030, All rights reserved.
 *
 *
 */

package com.iwindplus.base.redis.manager;

import cn.hutool.core.date.DateUnit;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.text.CharSequenceUtil;
import cn.hutool.core.util.StrUtil;
import com.iwindplus.base.domain.constant.CommonConstant.DateConstant;
import com.iwindplus.base.domain.constant.CommonConstant.NumberConstant;
import com.iwindplus.base.domain.enums.BizCodeEnum;
import com.iwindplus.base.domain.exception.BizException;
import com.iwindplus.base.redis.domain.constant.RedisConstant;
import com.iwindplus.base.redis.domain.enums.RedissonLockType;
import jakarta.annotation.Resource;
import java.time.Duration;
import java.time.LocalDateTime;
import java.util.Date;
import java.util.Optional;
import java.util.concurrent.TimeUnit;
import lombok.extern.slf4j.Slf4j;
import org.redisson.api.RLock;

/**
 * Redis生成流水号管理器（分布式锁方式）.
 *
 * @author zengdegui
 * @since 2018/9/5
 */
@Slf4j
public class SerialNumManager {

    @Resource
    private RedissonManager redissonManager;

    /**
     * 生成流水号.
     *
     * @param businessKey （可选，如：order）
     * @return String
     */
    public String getSerialNum(String businessKey) {
        return this.getSerialNum(businessKey, Boolean.TRUE);
    }

    /**
     * 生成固定日期格式的序列号（每天重置序号，补零长度6位）.
     *
     * @param businessKey （可选，如：order）
     * @return String
     */
    public String getSerialNumDate(String businessKey) {
        return this.getSerialNumDate(businessKey, Boolean.TRUE);
    }

    /**
     * 生成固定日期格式的序列号（补零长度6位）.
     *
     * @param businessKey 业务标识（如：order）
     * @param dailyReset  是否每天重置序号（可选）
     * @return String
     */
    public String getSerialNumDate(String businessKey, boolean dailyReset) {
        return this.getSerialNumDate(businessKey, NumberConstant.NUMBER_SIX, dailyReset);
    }

    /**
     * 生成序列号.
     *
     * @param businessKey 业务标识（如：order）
     * @param dailyReset  是否每天重置序号（可选）
     * @return String
     */
    public String getSerialNum(String businessKey, boolean dailyReset) {
        return this.getSerialNum(businessKey, NumberConstant.NUMBER_SIX, dailyReset);
    }

    /**
     * 生成固定日期格式的序列号.
     *
     * @param businessKey 业务标识（如：order）
     * @param length      补零长度（可选）
     * @param dailyReset  是否每天重置序号（可选）
     * @return String
     */
    public String getSerialNumDate(String businessKey, int length, boolean dailyReset) {
        return this.generateSerialNumDate(businessKey, DateConstant.YYMMDD, length, dailyReset);
    }

    /**
     * 生成序列号.
     *
     * @param businessKey 业务标识（如：order）
     * @param length      补零长度（可选）
     * @param dailyReset  是否每天重置序号（可选）
     * @return String
     */
    public String getSerialNum(String businessKey, int length, boolean dailyReset) {
        return this.generate(businessKey, null, length, dailyReset);
    }

    /**
     * 生成指定日期格式的序列号.
     *
     * @param businessKey 业务标识（可选，如：order）
     * @param datePattern 日期格式（如："yyMMdd"）
     * @param length      补零长度（可选）
     * @param dailyReset  是否每天重置序号（可选）
     * @return String
     */
    public String generateSerialNumDate(String businessKey, String datePattern, int length, boolean dailyReset) {
        datePattern = Optional.ofNullable(datePattern).orElse(DateConstant.YYMMDD);
        String childBusinessKey = DateUtil.format(LocalDateTime.now(), datePattern);
        return this.generate(businessKey, childBusinessKey, length, dailyReset);
    }

    /**
     * 生成序列号.
     *
     * @param businessKey      业务标识（可选，如：order）
     * @param childBusinessKey 子业务标识（可选，如："yyMMdd"）
     * @param length           补零长度（可选）
     * @param dailyReset       是否每天重置序号（可选）
     * @return String
     */
    private String generate(String businessKey,
        String childBusinessKey,
        int length,
        boolean dailyReset) {
        String bizKey = CharSequenceUtil.blankToDefault(businessKey, "default");
        String incKey = buildIncKey(bizKey, childBusinessKey);
        String prefix = buildPrefix(bizKey, childBusinessKey);
        String lockKey = RedisConstant.LOCK_KEY_PREFIX.concat(incKey);

        RLock lock = redissonManager.getLock(RedissonLockType.LOCK, lockKey);

        /* 重试间隔：第一次 100 ms，第二次 200 ms，第三次 300 ms，最长累计 600 ms */
        long[] waitMillis = {100, 200, 300};

        for (int attempt = 0; attempt < waitMillis.length; attempt++) {
            try {
                // 一次性抢锁，最多等 waitMillis[attempt] 毫秒；租约 5 秒足够业务完成
                if (lock.tryLock(waitMillis[attempt], 5_000, TimeUnit.MILLISECONDS)) {
                    long value = getValue(dailyReset, incKey);
                    return getCompletionSerialNum(prefix, value, length);
                }
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
                log.warn("生成序列号被中断，businessKey={}", businessKey);
                break;
            } catch (Exception ex) {
                log.error("生成序列号失败，businessKey={}", businessKey, ex);
                throw new BizException(BizCodeEnum.GENERATE_SERIAL_NUM_ERROR, new Object[]{lockKey});
            } finally {
                redissonManager.unlock(lock);
            }
        }

        log.error("获取分布式锁失败，重试 {} 次后仍失败，lockKey={}", waitMillis.length, lockKey);
        throw new BizException(BizCodeEnum.GET_LOCK_ERROR, new Object[]{lockKey});
    }

    /**
     * 构建序列号key.
     *
     * @param bizKey   业务标识
     * @param childKey 子业务标识
     * @return String
     */
    private String buildIncKey(String bizKey, String childKey) {
        return CharSequenceUtil.isBlank(childKey)
            ? StrUtil.format("serialNum:{}", bizKey)
            : StrUtil.format("serialNum:{}:{}", bizKey, childKey);
    }

    /**
     * 构建序列号前缀.
     *
     * @param bizKey   业务标识
     * @param childKey 子业务标识
     * @return String
     */
    private String buildPrefix(String bizKey, String childKey) {
        return CharSequenceUtil.isBlank(childKey) ? bizKey : bizKey.concat(childKey);
    }

    /**
     * 获取当前值.
     *
     * @param dailyReset 是否每天重置序号（可选）
     * @param incKey     序列号key
     * @return long
     */
    private long getValue(boolean dailyReset, String incKey) {
        if (dailyReset) {
            long ttl = this.getSecondsUntilNextDay() + 1;
            return redissonManager.incrementAndGetWithExpire(incKey, Duration.ofSeconds(ttl));
        }

        return redissonManager.incrementAndGet(incKey);
    }

    /**
     * 将数值拼接成指定长度的字符串（补零）.
     *
     * @param prefix 前缀（如日期）
     * @param value  当前递增值
     * @param length 补零长度
     * @return String
     */
    private String getCompletionSerialNum(String prefix, long value, int length) {
        return prefix.concat(String.format("%0" + length + "d", value));
    }

    /**
     * 获取当天剩余秒数，用于设置 TTL.
     *
     * @return long
     */
    private long getSecondsUntilNextDay() {
        Date now = new Date();
        Date endOfDay = DateUtil.endOfDay(now);
        return DateUtil.between(now, endOfDay, DateUnit.SECOND);
    }
}
